// 聊天相关功能
const $status = document.querySelector('#status');
const $connectButton = document.querySelector('#connect');
const $log = document.querySelector('#log');
const $form = document.querySelector('#chatform');
const $input = document.querySelector('#text');
const $imageBtn = document.querySelector('#chat-send-image');
const $cameraBtn = document.querySelector('#chat-send-camera');
const $videoBtn = document.querySelector('#chat-send-video');
const $audioBtn = document.querySelector('#chat-send-audio');
const $imageInput = document.querySelector('#chat-image-input');
const $cameraInput = document.querySelector('#chat-camera-input');
const $videoInput = document.querySelector('#chat-video-input');
const $audioInput = document.querySelector('#chat-audio-input');
// 拍照弹窗元素与状态
const $cameraModal = document.getElementById('cameraCaptureModal');
const $cameraPreview = document.getElementById('cameraPreview');
const $cameraCanvas = document.getElementById('cameraCanvas');
const $cameraCaptureBtn = document.getElementById('cameraCaptureBtn');
const $cameraSwitchBtn = document.getElementById('cameraSwitchBtn');
let cameraStream = null;
let cameraFacing = 'environment';
const $serverAddress = document.querySelector('#server-address');
const $currentRoom = document.querySelector('#current-room');
const $currentName = document.querySelector('#current-name');
const $usernameModal = document.getElementById('usernameModal');
const $usernameInput = document.getElementById('usernameInput');
const $usernameForm = document.getElementById('usernameForm');
const $confirmUsername = document.getElementById('confirmUsername');
const modal = new bootstrap.Modal($usernameModal);
// 媒体处理忙碌状态，避免重复弹窗或重复处理
const mediaBusy = { image: false, video: false, audio: false };
// 音频录制相关状态
let isRecordingAudio = false;
let audioRecorder = null;
let audioChunks = [];

/** @type {WebSocket | null} */
let socket = null;
let currentRoom = "默认";
let currentName = "未设置";
let needsToJoinRoom = false;

// 日志记录函数
function log(msg, type = 'status') {
  const timestamp = new Date().toLocaleTimeString('zh-CN');
  
  // 创建消息容器
  const msgContainer = document.createElement('div');
  
  // 根据消息类型设置样式
  if (type === 'status') {
    msgContainer.className = 'message-container system';
    msgContainer.innerHTML = msg;
  } else if (type === 'message') {
    // 检查是否为发送的消息
    if (msg.startsWith('我: ') || msg.startsWith('[发送给')) {
      msgContainer.className = msg.startsWith('[发送给') ? 'message-container sent-private' : 'message-container sent';
      // 移除前缀"我: "
      const displayMsg = msg.startsWith('我: ') ? msg.substring(3) : msg;
      msgContainer.innerHTML = `
        <div>${displayMsg}</div>
        <div class="message-time">${timestamp}</div>
      `;
    } else {
      // 检查是否为私信或其他接收的消息
      if (msg.startsWith('[私信]')) {
        msgContainer.className = 'message-container private';
      } else if (msg.startsWith('--- 历史消息')) {
        msgContainer.className = 'message-container system';
        msgContainer.innerHTML = msg;
      } else {
        msgContainer.className = 'message-container received';
        msgContainer.innerHTML = `
          <div>${msg}</div>
          <div class="message-time">${timestamp}</div>
        `;
      }
    }
  } else if (type === 'error') {
    msgContainer.className = 'message-container error';
    msgContainer.innerHTML = `错误: ${msg}`;
  }
  
  $log.appendChild(msgContainer);
  $log.scrollTop = $log.scrollHeight;
}

// 连接函数
function connect() {
  disconnect();

  const { location } = window;
  const proto = location.protocol.startsWith('https') ? 'wss' : 'ws';
  const wsUri = `${proto}://${location.host}/ws`;
  $serverAddress.textContent = wsUri;

  log('正在连接...');
  socket = new WebSocket(wsUri);

  socket.onopen = () => {
    log('已连接到聊天服务器');
    updateConnectionStatus();
    // 连接成功后显示用户名输入模态框
    needsToJoinRoom = true;
    modal.show();
  };

  socket.onmessage = ev => {
    // 处理消息，更新房间和名称信息
    const msg = ev.data;
    const trimmed = typeof msg === 'string' ? msg.trim() : '';
    // 媒体JSON消息识别与渲染
    if (trimmed.startsWith('{')) {
      try {
        const obj = JSON.parse(trimmed);
        if (obj && obj.type === 'media' && obj.url && obj.media_type) {
          renderMediaMessage(obj);
          return;
        }
      } catch (e) {
        // 非JSON或解析失败，继续走文本逻辑
      }
    }
    if (msg.startsWith('joined')) {
      log(`已加入房间: ${currentRoom}`);
    } else {
      log(msg, 'message');
    }
    
    // 更新用户名和房间信息显示
    if (msg.startsWith('/name')) {
      const parts = msg.split(' ');
      if (parts.length >= 2) {
        currentName = parts[1];
        $currentName.textContent = currentName;
      }
    }
  };

  socket.onclose = () => {
    log('已断开连接');
    socket = null;
    updateConnectionStatus();
  };

  socket.onerror = (error) => {
    log(`连接错误: ${error}`, 'error');
    updateConnectionStatus();
  };
}

// 断开连接函数
function disconnect() {
  if (socket) {
    log('正在断开连接...');
    socket.close();
    socket = null;
    updateConnectionStatus();
  }
}

// 更新连接状态
function updateConnectionStatus() {
  if (socket) {
    $status.className = 'badge bg-success text-white me-2';
    $status.textContent = '已连接';
    $connectButton.textContent = '断开连接';
    $connectButton.className = 'btn btn-light btn-sm';
    $input.disabled = false;
    $input.focus();
  } else {
    $status.className = 'badge bg-danger text-white me-2';
    $status.textContent = '未连接';
    $connectButton.textContent = '连接';
    $connectButton.className = 'btn btn-light btn-sm';
    $input.disabled = true;
  }
}

// 设置用户名并加入房间
function setUsernameAndJoin() {
  const username = $usernameInput.value.trim();
  if (username && socket && needsToJoinRoom) {
    currentName = username;
    $currentName.textContent = currentName;
    
    // 发送设置用户名命令
    socket.send(`/name ${username}`);
    // 发送加入房间命令
    socket.send(`/join ${currentRoom}`);
    
    log(`欢迎 ${username}，您已进入 ${currentRoom} 房间`);
    needsToJoinRoom = false;
    modal.hide();
    $usernameInput.value = '';
  }
}

// 连接按钮点击事件
$connectButton.addEventListener('click', () => {
  if (socket) {
    disconnect();
  } else {
    connect();
  }
});

// 表单提交事件
$form.addEventListener('submit', ev => {
  ev.preventDefault();

  const text = $input.value.trim();
  if (!text) return;

  // 处理特定命令
  if (text.startsWith('/join')) {
    const parts = text.split(' ');
    if (parts.length >= 2) {
      currentRoom = parts[1];
      $currentRoom.textContent = currentRoom;
    }
  } else if (text.startsWith('/name')) {
    const parts = text.split(' ');
    if (parts.length >= 2) {
      currentName = parts[1];
      $currentName.textContent = currentName;
    }
  }

  // 发送消息
  if (socket) {
    socket.send(text);
    // 如果不是命令，则显示发送的消息
    if (!text.startsWith('/')) {
      log(`我: ${text}`, 'message');
    } else if (text.startsWith('/to')) {
      // 特殊处理私信命令的显示
      const parts = text.split(' ', 3);
      if (parts.length >= 3) {
        log(`[发送给 ${parts[1]}] ${parts[2]}`, 'message');
      }
    }
    $input.value = '';
    $input.focus();
  } else {
    log('未连接到服务器，请先点击"连接"按钮', 'error');
  }
});

// 确认用户名按钮点击事件
$confirmUsername.addEventListener('click', setUsernameAndJoin);

// 用户名表单提交事件
$usernameForm.addEventListener('submit', (e) => {
  e.preventDefault();
  setUsernameAndJoin();
});

// 模态框关闭事件
$usernameModal.addEventListener('hidden.bs.modal', () => {
  // 如果用户关闭了模态框但未设置用户名，提示用户
  if (socket && needsToJoinRoom) {
    log('请设置您的昵称以加入聊天室', 'error');
  }
});

// 页面加载完成后执行聊天相关初始化
function initChat() {
  updateConnectionStatus();
  bindMediaControls();
}

// 导出初始化函数
window.initChat = initChat;

// 统一的 getUserMedia 兼容封装（支持旧版前缀与不支持场景的明确错误）
async function getUserMediaCompat(constraints) {
  const md = navigator.mediaDevices;
  if (md && typeof md.getUserMedia === 'function') {
    return md.getUserMedia(constraints);
  }
  const legacy = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
  if (legacy) {
    return new Promise((resolve, reject) => legacy.call(navigator, constraints, resolve, reject));
  }
  throw new Error('getUserMedia 不受支持');
}

// 显示麦克风权限引导横幅
function showMicPermissionBanner() {
  if (document.querySelector('.permission-banner')) return;
  const banner = document.createElement('div');
  banner.className = 'permission-banner';
  banner.innerHTML = `
    <div class="permission-text">
      麦克风权限被阻止。请点击地址栏左侧的锁图标，找到“麦克风”，设置为“允许”，然后刷新或再次点击录音按钮。
    </div>
    <div class="permission-actions">
      <button type="button" class="permission-close">知道了</button>
    </div>
  `;
  document.body.appendChild(banner);
  const closeBtn = banner.querySelector('.permission-close');
  closeBtn && closeBtn.addEventListener('click', () => {
    try { banner.remove(); } catch (_) {}
  });
}

// 绑定媒体发送控件
function bindMediaControls() {
  if ($imageBtn) {
    $imageBtn.addEventListener('click', () => {
      if ($imageInput) {
        $imageInput.click();
      } else {
        // 兜底：未找到输入控件时动态创建
        const tmp = document.createElement('input');
        tmp.type = 'file';
        tmp.accept = 'image/*';
        tmp.style.display = 'none';
        document.body.appendChild(tmp);
        tmp.addEventListener('change', () => {
          handleFileSelected(tmp, 'image');
          // 选择完成后移除临时输入控件
          document.body.removeChild(tmp);
        });
        tmp.click();
      }
    });
  }
  if ($videoBtn) {
    $videoBtn.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      if (mediaBusy.video) return;
      if ($videoInput) {
        $videoInput.click();
      } else {
        const tmp = document.createElement('input');
        tmp.type = 'file';
        tmp.accept = 'video/*';
        tmp.style.display = 'none';
        document.body.appendChild(tmp);
        tmp.addEventListener('change', () => {
          handleFileSelected(tmp, 'video');
          document.body.removeChild(tmp);
        });
        tmp.click();
      }
    });
  }
  // 拍照按钮：尽可能调用系统相机（移动端）
  if ($cameraBtn) {
    $cameraBtn.addEventListener('click', () => {
      openCameraModal();
    });
  }
  if ($audioBtn) {
    $audioBtn.addEventListener('click', async () => {
      // 点击音频按钮：开始/停止麦克风录音并发送
      if (!isRecordingAudio) {
        try {
          // 先尝试查询权限状态（部分浏览器支持），若已被永久拒绝则直接提示
          if (navigator.permissions && navigator.permissions.query) {
            try {
              const status = await navigator.permissions.query({ name: 'microphone' });
              if (status && status.state === 'denied') {
                showMicPermissionBanner();
                return;
              }
            } catch (_) { /* 忽略不支持或异常 */ }
          }
          // 在不安全上下文或旧版浏览器上进行提示
          const isSecure = window.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1';
          if (!isSecure) {
            log('提示：麦克风访问通常需要在 https 或 localhost。当前可能被浏览器限制。', 'status');
          }

          const stream = await getUserMediaCompat({ audio: true });
          audioChunks = [];
          if (typeof MediaRecorder === 'undefined') {
            // 无 MediaRecorder 支持：回退到文件录音（移动端通常可调起系统录音）
            log('此浏览器不支持实时录音(MediaRecorder)。已回退到系统录音。', 'status');
            try { stream.getTracks().forEach(t => t.stop()); } catch(_){}
            if ($audioInput) {
              $audioInput.setAttribute('capture', 'microphone');
              try { $audioInput.click(); } catch(_){}
            } else {
              const tmp = document.createElement('input');
              tmp.type = 'file';
              tmp.accept = 'audio/*';
              tmp.setAttribute('capture', 'microphone');
              tmp.style.display = 'none';
              document.body.appendChild(tmp);
              tmp.addEventListener('change', () => {
                handleFileSelected(tmp, 'audio');
                document.body.removeChild(tmp);
              });
              tmp.click();
            }
            return;
          }

          let options = undefined;
          try {
            if (typeof MediaRecorder.isTypeSupported === 'function') {
              if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
                options = { mimeType: 'audio/webm;codecs=opus' };
              } else if (MediaRecorder.isTypeSupported('audio/webm')) {
                options = { mimeType: 'audio/webm' };
              } else if (MediaRecorder.isTypeSupported('audio/mp4')) {
                options = { mimeType: 'audio/mp4' };
              }
            } else {
              options = { mimeType: 'audio/webm;codecs=opus' };
            }
          } catch(_) {}

          const recorder = new MediaRecorder(stream, options);
          audioRecorder = recorder;
          recorder.ondataavailable = (e) => {
            if (e.data && e.data.size > 0) audioChunks.push(e.data);
          };
          recorder.onstop = async () => {
            try {
              const mime = (options && options.mimeType) ? options.mimeType : 'audio/webm';
              const blob = new Blob(audioChunks, { type: mime });
              // 将录音Blob转换为File以复用上传逻辑
              const ts = Date.now();
              const ext = mime.includes('mp4') ? 'm4a' : 'webm';
              const file = new File([blob], `recording_${ts}.${ext}`, { type: mime });
              // 复用处理函数：构造一个临时input模拟（仅用于传参），不插入DOM
              const fakeInput = { files: [file], value: '' };
              handleFileSelected(fakeInput, 'audio');
            } catch (err) {
              log(`录音处理失败: ${err}`, 'error');
            } finally {
              // 关闭麦克风轨道
              try { stream.getTracks().forEach(t => t.stop()); } catch(_){}
              isRecordingAudio = false;
              audioRecorder = null;
              audioChunks = [];
              $audioBtn.innerHTML = '<i class="fa fa-music"></i> 音频';
              $audioBtn.classList.remove('btn-danger');
            }
          };
          recorder.start();
          isRecordingAudio = true;
          $audioBtn.innerHTML = '<i class="fa fa-stop"></i> 停止录音';
          $audioBtn.classList.add('btn-danger');
          log('开始录音...再次点击“停止录音”结束并发送');
        } catch (err) {
          log(`无法访问麦克风：${err}`, 'error');
          // 被用户拒绝或浏览器阻止权限时，给出明确指引
          if (err && (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError')) {
            showMicPermissionBanner();
          } else if (String(err).includes('getUserMedia 不受支持') || (err && err.name === 'TypeError')) {
            // 完全不支持 getUserMedia：使用文件录音兜底
            if ($audioInput) {
              $audioInput.setAttribute('capture', 'microphone');
              try { $audioInput.click(); } catch(_){}
            } else {
              const tmp = document.createElement('input');
              tmp.type = 'file';
              tmp.accept = 'audio/*';
              tmp.setAttribute('capture', 'microphone');
              tmp.style.display = 'none';
              document.body.appendChild(tmp);
              tmp.addEventListener('change', () => {
                handleFileSelected(tmp, 'audio');
                document.body.removeChild(tmp);
              });
              tmp.click();
            }
          }
        }
      } else {
        // 正在录音，点击则停止并触发上传
        try {
          audioRecorder && audioRecorder.stop();
        } catch (err) {
          log(`停止录音失败：${err}`, 'error');
        }
      }
    });
  }

  if ($imageInput) {
    $imageInput.addEventListener('change', () => handleFileSelected($imageInput, 'image'));
  }
  if ($cameraInput) {
    $cameraInput.addEventListener('change', () => handleFileSelected($cameraInput, 'image'));
  }
  if ($videoInput) {
    $videoInput.addEventListener('change', () => handleFileSelected($videoInput, 'video'));
  }
  if ($audioInput) {
    $audioInput.addEventListener('change', () => handleFileSelected($audioInput, 'audio'));
  }

}

// 打开拍照弹窗并请求摄像头
async function openCameraModal() {
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    try {
      const constraints = { video: { facingMode: { ideal: cameraFacing }, width: { ideal: 1280 }, height: { ideal: 720 } } };
      stopCamera();
      const stream = await navigator.mediaDevices.getUserMedia(constraints);
      cameraStream = stream;
      if ($cameraPreview) {
        $cameraPreview.srcObject = stream;
      }
      const modalInstance = ($cameraModal && window.bootstrap && bootstrap.Modal)
        ? bootstrap.Modal.getOrCreateInstance($cameraModal)
        : null;
      if (modalInstance) {
        modalInstance.show();
      } else {
        log('拍照弹窗未准备好，已回退到系统相机选择。', 'status');
        // 回退：打开系统相机/文件选择
        if ($cameraInput) {
          try { $cameraInput.click(); } catch(_){}
        }
        // 释放摄像头流，避免占用
        stopCamera();
      }
    } catch (err) {
      log(`无法访问摄像头：${err}`, 'error');
      // 兜底：使用带capture的input在支持的移动端打开系统相机
      if ($cameraInput) {
        try { $cameraInput.click(); } catch(_){}
      }
    }
  } else {
    // 不支持getUserMedia：尝试使用capture属性的文件输入
    if ($cameraInput) {
      try { $cameraInput.click(); } catch(_){}
    } else {
      const tmp = document.createElement('input');
      tmp.type = 'file';
      tmp.accept = 'image/*';
      tmp.setAttribute('capture', 'environment');
      tmp.style.display = 'none';
      document.body.appendChild(tmp);
      tmp.addEventListener('change', () => {
        handleFileSelected(tmp, 'image');
        document.body.removeChild(tmp);
      });
      tmp.click();
    }
  }
}

function stopCamera() {
  try {
    if (cameraStream) {
      cameraStream.getTracks().forEach(t => t.stop());
      cameraStream = null;
    }
    if ($cameraPreview) {
      $cameraPreview.srcObject = null;
    }
  } catch(_){}
}

// 拍照弹窗事件：关闭时停止摄像头
if ($cameraModal) {
  $cameraModal.addEventListener('hidden.bs.modal', () => {
    stopCamera();
  });
}

// 切换前后镜头
if ($cameraSwitchBtn) {
  $cameraSwitchBtn.addEventListener('click', async () => {
    cameraFacing = cameraFacing === 'environment' ? 'user' : 'environment';
    try {
      const constraints = { video: { facingMode: { ideal: cameraFacing }, width: { ideal: 1280 }, height: { ideal: 720 } } };
      stopCamera();
      const stream = await navigator.mediaDevices.getUserMedia(constraints);
      cameraStream = stream;
      if ($cameraPreview) {
        $cameraPreview.srcObject = stream;
      }
    } catch (err) {
      log(`切换镜头失败：${err}`, 'error');
    }
  });
}

// 拍照并发送
if ($cameraCaptureBtn) {
  $cameraCaptureBtn.addEventListener('click', async () => {
    try {
      const video = $cameraPreview;
      if (!video || !video.videoWidth || !video.videoHeight) {
        throw new Error('摄像头未就绪');
      }
      const w = video.videoWidth;
      const h = video.videoHeight;
      const canvas = $cameraCanvas;
      canvas.width = w;
      canvas.height = h;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(video, 0, 0, w, h);
      canvas.toBlob((blob) => {
        if (!blob) {
          log('拍照失败：无法获取图像数据', 'error');
          return;
        }
        const ts = Date.now();
        const file = new File([blob], `camera_${ts}.jpg`, { type: 'image/jpeg' });
        const fakeInput = { files: [file], value: '' };
        handleFileSelected(fakeInput, 'image');
        const modalInstance = ($cameraModal && window.bootstrap && bootstrap.Modal)
          ? bootstrap.Modal.getOrCreateInstance($cameraModal)
          : null;
        if (modalInstance) {
          modalInstance.hide();
        }
        stopCamera();
      }, 'image/jpeg', 0.92);
    } catch (err) {
      log(`拍照失败：${err}`, 'error');
    }
  });
}

function handleFileSelected(inputEl, mediaType) {
  const file = inputEl.files && inputEl.files[0];
  if (!file) return;
  if (mediaBusy[mediaType]) return;
  mediaBusy[mediaType] = true;

  const MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50MB 与后端一致
  if (file.size > MAX_UPLOAD_BYTES) {
    log(`文件过大：${(file.size/1024/1024).toFixed(1)}MB，最大允许50MB`, 'error');
    mediaBusy[mediaType] = false;
    return;
  }

  const reader = new FileReader();
  reader.onload = async (e) => {
    let base64 = e.target.result;
    // 大文件使用ArrayBuffer转换为纯base64，避免dataURL前缀带来额外开销
    if (mediaType === 'video' || mediaType === 'audio') {
      if (base64 instanceof ArrayBuffer) {
        const bytes = new Uint8Array(base64);
        const chunkSize = 0x8000;
        let binary = '';
        for (let i = 0; i < bytes.length; i += chunkSize) {
          binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize));
        }
        base64 = btoa(binary);
      }
    }
    try {
      log(`正在上传${mediaType}...`);
      const resp = await fetch('/api/chat/upload', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ filename: file.name, content_base64: base64, media_type: mediaType })
      });
      if (!resp.ok) {
        const txt = await resp.text();
        throw new Error(txt);
      }
      const data = await resp.json();
      const msgObj = { type: 'media', media_type: mediaType, url: data.url };
      if (socket) {
        socket.send(JSON.stringify(msgObj));
      } else {
        log('未连接到服务器，已本地预览。', 'status');
      }
      renderMediaMessage(msgObj, true);
    } catch (err) {
      log(`媒体上传失败: ${err}`, 'error');
    } finally {
      inputEl.value = '';
      mediaBusy[mediaType] = false;
    }
  };
  if (mediaType === 'video' || mediaType === 'audio') {
    reader.readAsArrayBuffer(file);
  } else {
    reader.readAsDataURL(file);
  }
}

function renderMediaMessage(obj, isSelf = false) {
  const timestamp = new Date().toLocaleTimeString('zh-CN');
  const container = document.createElement('div');
  const baseClass = isSelf ? 'message-container sent' : 'message-container received';
  const isMediaNoPadding = obj.media_type === 'image' || obj.media_type === 'video';
  container.className = isMediaNoPadding ? `${baseClass} media` : baseClass;

  let mediaEl = null;
  if (obj.media_type === 'image') {
    mediaEl = document.createElement('img');
    mediaEl.src = obj.url;
    mediaEl.className = 'chat-image';
    mediaEl.addEventListener('click', () => {
      const modalEl = document.getElementById('imagePreviewModal');
      const imgEl = document.getElementById('imagePreviewImg');
      if (modalEl && imgEl) {
        imgEl.src = obj.url;
        const modalInstance = bootstrap.Modal.getOrCreateInstance(modalEl);
        modalInstance.show();
      }
    });
  } else if (obj.media_type === 'video') {
    mediaEl = document.createElement('video');
    mediaEl.src = obj.url;
    mediaEl.controls = true;
    mediaEl.className = 'chat-video';
  } else if (obj.media_type === 'audio') {
    const audioEl = document.createElement('audio');
    audioEl.src = obj.url;
    audioEl.preload = 'metadata';
    audioEl.className = 'chat-audio';

    const bubble = document.createElement('div');
    bubble.className = 'audio-bubble';

    const playBtn = document.createElement('button');
    playBtn.type = 'button';
    playBtn.className = 'audio-play';
    playBtn.innerHTML = '<i class="fa fa-play"></i>';

    const infoBox = document.createElement('div');
    infoBox.className = 'audio-info';

    const timeBox = document.createElement('div');
    timeBox.className = 'audio-time';
    timeBox.textContent = '0:00';

    const progress = document.createElement('div');
    progress.className = 'audio-progress';
    const progressBar = document.createElement('div');
    progressBar.className = 'audio-progress-bar';
    progress.appendChild(progressBar);

    infoBox.appendChild(timeBox);
    infoBox.appendChild(progress);

    bubble.appendChild(playBtn);
    bubble.appendChild(infoBox);
    bubble.appendChild(audioEl);

    // 事件绑定
    const formatTime = (sec) => {
      if (!isFinite(sec)) return '0:00';
      const m = Math.floor(sec / 60);
      const s = Math.floor(sec % 60);
      return `${m}:${s.toString().padStart(2,'0')}`;
    };

    let duration = NaN;
    audioEl.addEventListener('loadedmetadata', () => {
      duration = audioEl.duration;
      timeBox.textContent = `0:00 / ${formatTime(duration)}`;
    });

    audioEl.addEventListener('timeupdate', () => {
      const cur = audioEl.currentTime;
      timeBox.textContent = `${formatTime(cur)} / ${formatTime(duration)}`;
      const ratio = duration ? (cur / duration) : 0;
      progressBar.style.width = `${Math.max(0, Math.min(1, ratio)) * 100}%`;
    });

    audioEl.addEventListener('ended', () => {
      playBtn.innerHTML = '<i class="fa fa-play"></i>';
      progressBar.style.width = '0%';
    });

    playBtn.addEventListener('click', () => {
      if (audioEl.paused) {
        audioEl.play().then(() => {
          playBtn.innerHTML = '<i class="fa fa-pause"></i>';
        }).catch(err => {
          log(`音频播放失败: ${err}`, 'error');
        });
      } else {
        audioEl.pause();
        playBtn.innerHTML = '<i class="fa fa-play"></i>';
      }
    });

    // 点击进度条跳转
    progress.addEventListener('click', (e) => {
      const rect = progress.getBoundingClientRect();
      const ratio = (e.clientX - rect.left) / rect.width;
      if (isFinite(duration) && duration > 0) {
        audioEl.currentTime = Math.max(0, Math.min(1, ratio)) * duration;
      }
    });

    mediaEl = bubble;
  }

  const timeEl = document.createElement('div');
  timeEl.className = 'message-time';
  timeEl.textContent = timestamp;

  if (mediaEl) {
    container.appendChild(mediaEl);
    container.appendChild(timeEl);
    $log.appendChild(container);
    $log.scrollTop = $log.scrollHeight;
  } else {
    // 回退到文本展示
    log(JSON.stringify(obj), isSelf ? 'message' : 'message');
  }
}